﻿/* ****************************************************************************
 *
 * Copyright (c) Microsoft Corporation. 
 *
 * This source code is subject to terms and conditions of the Microsoft Public License. A
 * copy of the license can be found at http://go.microsoft.com/fwlink/?LinkID=131993. If
 * you cannot locate the Microsoft Public License, please send an email to
 * mixon@microsoft.com. By using this source code in any fashion, you are agreeing to
 * be bound by the terms of the Microsoft Public License.
 *
 * You must not remove this notice, or any other, from this software.
 *
 *
 * ***************************************************************************/


using System;
using System.Collections;
using System.Collections.Specialized;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;

namespace Microsoft.Controls
{
    /// <summary>
    /// Courtesy of the Silverlight team
    /// </summary>
    [TemplatePart(Name = "ItemsPanel", Type = typeof(VirtualizingPanel))]
    [TemplatePart(Name = "VerticalScrollBar", Type = typeof(ScrollBar))]
    [TemplatePart(Name = "HorizontalScrollBar", Type = typeof(ScrollBar))]
    public class VirtualizedItemsControl : Control, IGenerateData
    {
        protected VirtualizingStackPanel ItemsPanel { get; private set; }
        private ScrollBar verticalScrollBar;
        private ScrollBar horizontalScrollBar;

        internal event EventHandler<ScrolledEventArgs> Scrolled;

        private int firstVisibleItemIndex;
        public int FirstVisibleItemIndex
        {
            get { return firstVisibleItemIndex; }
            protected set
            {
                this.firstVisibleItemIndex = value;
                this.verticalScrollBar.Value = value;
            }
        }
        public double FirstVisibleHorizontalPixel { get; private set; }

        private int fullyVisibleItemCount;
        public int FullyVisibleItemCount
        {
            get { return this.fullyVisibleItemCount; }
            set
            {
                this.fullyVisibleItemCount = value;
                this.verticalScrollBar.ViewportSize = value;
                this.verticalScrollBar.LargeChange = value;
                this.UpdateVerticalScrollBar();
            }
        }

        private double visibleHorizontalPixelCount;
        public double VisibleHorizontalPixelCount
        {
            get { return this.visibleHorizontalPixelCount; }
            set
            {
                this.visibleHorizontalPixelCount = value;
                this.horizontalScrollBar.ViewportSize = value;
                this.horizontalScrollBar.LargeChange = value;
                this.UpdateHorizontalScrollBar();
            }
        }

        private int totalItemsCount;
        public int TotalItemsCount
        {
            get { return this.totalItemsCount; }
            private set
            {
                this.totalItemsCount = value;
                this.UpdateVerticalScrollBar();
            }
        }

        private double totalHorizontalPixelCount;
        public double TotalHorizontalPixelCount
        {
            get { return this.totalHorizontalPixelCount; }
            set
            {
                this.totalHorizontalPixelCount = value;
                this.UpdateHorizontalScrollBar();
            }
        }

        public IList Items
        {
            get { return (IList)GetValue(ItemsProperty); }
            set { SetValue(ItemsProperty, value); }
        }

        public static readonly DependencyProperty ItemsProperty =
            DependencyProperty.Register("Items", typeof(IList), typeof(VirtualizedItemsControl), new PropertyMetadata(Items_PropertyChanged));

        public DataTemplate ItemTemplate
        {
            get { return (DataTemplate)GetValue(ItemTemplateProperty); }
            set { SetValue(ItemTemplateProperty, value); }
        }

        // TODO: Add a callback for ItemTemplate changes. Change the ContentTemplates of all children when this changes.
        public static readonly DependencyProperty ItemTemplateProperty =
            DependencyProperty.Register("ItemTemplate", typeof(DataTemplate), typeof(VirtualizedItemsControl), new PropertyMetadata(null));

        public VirtualizedItemsControl()
        {
            this.DefaultStyleKey = typeof(VirtualizedItemsControl);
        }

        private static void Items_PropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            ((VirtualizedItemsControl)sender).ItemsChanged(e);
        }

        private void ItemsChanged(DependencyPropertyChangedEventArgs e)
        {
            this.TotalItemsCount = this.Items.Count;
 
            if (this.verticalScrollBar != null)
            {
                this.verticalScrollBar.Value = 0;
            }
            if (this.horizontalScrollBar != null)
            {
                this.horizontalScrollBar.Value = 0;
            }

            if (this.ItemsPanel != null)
            {
                this.ItemsPanel.InvalidateMeasure();
            }

            var oldNotifyCollection = e.OldValue as INotifyCollectionChanged;
            if (oldNotifyCollection != null)
            {
                oldNotifyCollection.CollectionChanged -= new NotifyCollectionChangedEventHandler(Items_CollectionChanged);
            }

            var newNotifyCollection = e.NewValue as INotifyCollectionChanged;
            if (newNotifyCollection != null)
            {
                newNotifyCollection.CollectionChanged += new NotifyCollectionChangedEventHandler(Items_CollectionChanged);
            }
        }

        private void Items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if ((e.NewItems != null && e.NewItems.Count > 1) ||
                (e.OldItems != null && e.OldItems.Count > 1))
            {
                throw new NotSupportedException("Cannot add or remove more than one item at a time.");
            }
            if (this.ItemsPanel != null)
            {
                switch (e.Action)
                {
                    case NotifyCollectionChangedAction.Add:
                        this.TotalItemsCount++;
                        break;
                    case NotifyCollectionChangedAction.Remove:
                        this.TotalItemsCount--;
                        break;
                    case NotifyCollectionChangedAction.Replace:
                        break;
                    case NotifyCollectionChangedAction.Reset:
                        this.TotalItemsCount = 0;
                        break;
                }
                this.ItemsPanel.InvalidateMeasure();
            }
        }

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            this.ItemsPanel = this.GetTemplateChild("ItemsPanel") as VirtualizingStackPanel;
            if (this.ItemsPanel == null)
            {
                throw new InvalidOperationException("Template for VirtualizedItemsControl must contain a VirtualizingPanel named ItemsPanel.");
            }
            this.ItemsPanel.DataGenerator = this;

            this.verticalScrollBar = this.GetTemplateChild("VerticalScrollBar") as ScrollBar;
            if (verticalScrollBar == null)
            {
                throw new InvalidOperationException("Template for VirtualizedItemsControl must contain a ScrollBar named VerticalScrollBar.");
            }
            this.verticalScrollBar.ValueChanged += new RoutedPropertyChangedEventHandler<double>(VerticalScrollBar_ValueChanged);

            this.horizontalScrollBar = this.GetTemplateChild("HorizontalScrollBar") as ScrollBar;
            if (horizontalScrollBar == null)
            {
                throw new InvalidOperationException("Template for VirtualizedItemsControl must contain a ScrollBar named HorizontalScrollBar.");
            }
            this.horizontalScrollBar.ValueChanged += new RoutedPropertyChangedEventHandler<double>(HorizontalScrollBar_ValueChanged);
        }

        private void VerticalScrollBar_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            var oldFirstVisibleItemIndex = this.firstVisibleItemIndex;
            this.FirstVisibleItemIndex = (int)(Math.Round(e.NewValue));
            if (oldFirstVisibleItemIndex < this.FirstVisibleItemIndex)
            {
                this.OnScrolled(new ScrolledEventArgs(Direction.Up));
            }
            if (oldFirstVisibleItemIndex > this.FirstVisibleItemIndex)
            {
                this.OnScrolled(new ScrolledEventArgs(Direction.Down));
            }
            this.ItemsPanel.InvalidateMeasure();
        }

        private void HorizontalScrollBar_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            this.FirstVisibleHorizontalPixel = e.NewValue;
            this.ItemsPanel.InvalidateArrange();
        }

        public virtual UIElement GetContainerForItemOverride()
        {
            return new ContentPresenter();
        }

        public virtual void PrepareContainerForItemOverride(UIElement container, object dataItem)
        {
            var containerContentPresenter = container as ContentPresenter;
            if (containerContentPresenter != null)
            {
                containerContentPresenter.ContentTemplate = this.ItemTemplate;
                containerContentPresenter.Content = dataItem;
                containerContentPresenter.DataContext = dataItem;
            }
        }

        public virtual void ClearContainerForItemOverride(UIElement container, object dataItem)
        {
            // TODO: Detach potential events from dataItem
        }

        object IGenerateData.GetDataItem(int index)
        {
            if (this.Items == null)
            {
                return null;
            }
            return this.Items[index];
        }

        private void UpdateHorizontalScrollBar()
        {
            if (this.horizontalScrollBar != null)
            {
                this.horizontalScrollBar.Maximum = this.TotalHorizontalPixelCount - this.VisibleHorizontalPixelCount;
                if (this.TotalHorizontalPixelCount <= this.VisibleHorizontalPixelCount)
                {
                    this.horizontalScrollBar.Visibility = Visibility.Collapsed;
                }
                else
                {
                    this.horizontalScrollBar.Visibility = Visibility.Visible;
                }
            }
        }

        private void UpdateVerticalScrollBar()
        {
            if (this.verticalScrollBar != null)
            {
                this.verticalScrollBar.Maximum = this.TotalItemsCount - this.FullyVisibleItemCount;
                if (this.TotalItemsCount <= this.FullyVisibleItemCount)
                {
                    this.verticalScrollBar.Visibility = Visibility.Collapsed;
                }
                else
                {
                    this.verticalScrollBar.Visibility = Visibility.Visible;
                }
            }
        }

        protected void ScrollHorizontally(bool scrollRight)
        {
            if (scrollRight)
            {
                this.horizontalScrollBar.Value = Math.Min(this.horizontalScrollBar.Value + this.horizontalScrollBar.SmallChange, this.horizontalScrollBar.Maximum);
            }
            else
            {
                this.horizontalScrollBar.Value = Math.Max(this.horizontalScrollBar.Value - this.horizontalScrollBar.SmallChange, this.horizontalScrollBar.Minimum);
            }
        }

        internal virtual void OnScrolled(ScrolledEventArgs e)
        {
            var eventHandler = this.Scrolled;
            if (eventHandler != null)
            {
                eventHandler(this, e);
            }
        }
    }

    public class ScrolledEventArgs : EventArgs
    {
        public Direction ScrollDirection { get; private set; }

        public ScrolledEventArgs(Direction scrollDirection)
        {
            this.ScrollDirection = scrollDirection;
        }
    }

    public enum Direction
    {
        Up,
        Down
    }
}
